use crate::dbs::sqlite::Peer;
use crate::features::ddd::Aggregate;
use crate::features::sgraph::modelsdbo::EdgeDBO;
use crate::features::tasks::modelsdbo::{TaskDBO, TaskDiffProbaDBO};
use crate::features::tasks::services;
use crate::features::tasks::{
  EBP_DIFF_INITIAL, EBP_DIFF_NORMAL, EBP_DIFF_ODEROUS, EBP_DIFF_REMEMBERED, EBP_PROBA_MIN,
};
use chrono::prelude::*;
use derivative::Derivative;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Derivative, Debug)]
#[derivative(Default)]
pub struct TaskConfig {
  pub dfpbs: Vec<TaskDiffProbaDBO>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TaskEventAnswered {
  pub std_answer: String,
  pub answer: String,
  pub seconds: u32,
  pub timestamp: String, // HLC format
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum TaskEvent {
  Answered(TaskEventAnswered),
}

impl TaskEvent {
  pub fn answered(std_answer: &str, answer: &str, seconds: u32, timestamp: &str) -> TaskEvent {
    TaskEvent::Answered(TaskEventAnswered {
      std_answer: std_answer.to_owned(),
      answer: answer.to_owned(),
      seconds: seconds.to_owned(),
      timestamp: timestamp.to_owned(),
    })
  }
}

#[derive(Serialize, Deserialize, Clone, Derivative, Debug)]
#[derivative(Default)]
pub struct TaskSnapshot {
  pub task: TaskDBO,
  pub edges: Vec<EdgeDBO>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TaskAggregate {
  id: String,
  snapshot: TaskSnapshot,
  config: TaskConfig,
}

/// command handler
impl TaskAggregate {
  /// command answer
  pub fn answer(
    &self,
    my_peer: &Peer,
    // standard official answer
    std_answer: &str,
    // your answer
    answer: &str,
    // you answered in seconds , typically for identifying the answer quality,
    // e.g. Excellent Quality within 2 seconds, Bad Quality within 15 seconds
    seconds: Option<u32>,
  ) -> Result<Vec<TaskEvent>, String> {
    Ok(vec![TaskEvent::answered(
      std_answer,
      answer,
      seconds.unwrap_or(0),
      &my_peer.timestamp(),
    )])
  }
}
impl Aggregate for TaskAggregate {
  type ID = String;
  type ItemEvent = TaskEvent;
  type Snapshot = TaskSnapshot;
  type Config = TaskConfig;

  fn init(snapshot: &TaskSnapshot, events: &Vec<TaskEvent>, config: &TaskConfig) -> TaskAggregate {
    let task_id = snapshot.task.id.to_owned();

    let mut task_agg = TaskAggregate {
      id: task_id,
      snapshot: snapshot.to_owned(),
      config: config.to_owned(),
    };

    for evt in events {
      task_agg = task_agg.apply(evt);
    }
    task_agg
  }

  fn apply(&self, evt: &TaskEvent) -> TaskAggregate {
    let now: DateTime<Utc> = Utc::now(); // e.g. `2014-11-28T12:45:59.324310806Z`
                                         // ebp_difficulty = 0,1,2,3, 4, 5,6,7,8
    let mut ebp_difficulty: usize = self.snapshot.task.ebp_difficulty;
    if ebp_difficulty == EBP_DIFF_INITIAL {
      ebp_difficulty = EBP_DIFF_NORMAL;
    }

    match evt {
      TaskEvent::Answered(TaskEventAnswered {
        ref std_answer,
        ref answer,
        ref seconds,
        ref timestamp,
        ..
      }) => {
        if std_answer == answer && ebp_difficulty > EBP_DIFF_REMEMBERED {
          // answer correct then easier
          ebp_difficulty -= 1;
        } else if std_answer != answer
          && EBP_DIFF_REMEMBERED <= ebp_difficulty
          && ebp_difficulty < EBP_DIFF_NORMAL
        {
          // asnwer fail anytime then reset default NORMAL
          ebp_difficulty = EBP_DIFF_NORMAL;
        } else if std_answer != answer
          && EBP_DIFF_NORMAL <= ebp_difficulty
          && ebp_difficulty < EBP_DIFF_ODEROUS
        {
          // consecutive answer fail then more difficulty
          ebp_difficulty += 1;
        }

        let dfpb =
          services::find_in_task_diffprob(ebp_difficulty, EBP_PROBA_MIN, &self.config.dfpbs);

        let now: DateTime<Utc> = Utc::now(); // e.g. `2014-11-28T12:45:59.324310806Z`

        let task_updated = TaskDBO {
          snapshot_at: timestamp.to_owned(),
          ebp_difficulty: ebp_difficulty,
          // refer to config table: task_diff_proba for probability
          ebp_forgetproba: EBP_PROBA_MIN,
          // TODO: fix me by setting ebp_recalltime as now.timestamp() + task_diff_proba[ebp_difficulty].interval
          ebp_recalltime: match dfpb {
            Some(v) => now.timestamp() + v.interval,
            _ => now.timestamp() + 5 * 60,
          },

          updated_at: now.timestamp(),
          ..self.snapshot.task.to_owned()
        };

        println!("timestamp={},task_updated={:?}", timestamp, task_updated);

        let mut snapshot_ = self.snapshot.clone();
        snapshot_.task = task_updated;

        TaskAggregate {
          snapshot: snapshot_,
          ..self.to_owned()
        }
      }
    }
  }

  fn get_id(&self) -> &String {
    &self.id
  }
  fn get_snapshot(&self) -> &TaskSnapshot {
    &self.snapshot
  }
}
